[Flutter]flutter_staggered_grid_viewを使って「Pinterest」風のレイアウトを実現する
こんにちは。CX事業本部の田辺です。この記事ではFlutterで写真共有サービス「Pinterest」風のグリッドレイアウトをFlutterで実装する方法を解説します。iOSで実現する方法については当ブログの過去の記事を参照してください。
環境について
pubspec.yamlの設定です。
environment: sdk: ">=2.7.0 <3.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.3 flutter_staggered_grid_view: ^0.3.1
flutter_staggered_grid_view
cross axisを等倍の大きさの複数に分割して空いているmain axisのスペースを基準に最適な位置に要素を配置していくグリッドレイアウトを実装することができるWidgetを提供しているパッケージです。
インストール方法
他のパッケージと同じくpubspec.yaml
ファイルに追記してflutter pub get
コマンドを実行することで導入できます。アプリケーションコードでimportすることでパッケージ内でexportされているウィジェットを使用できます。
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
アップデートによりインストール方法などが変わるかもしれないので実際に使用される際にはInstallingを確認することをすすめます。
主要なクラス
flutter_staggered_grid_viewパッケージで登場する主なクラスにを事前に紹介しておきます。
StaggeredGridView
スクロール可能な、可変サイズのウィジェット群を表示するクラスです。main axisは、スクロールする方向(scrollDirection)です。主要なコンストラクタは2つあります。決まった数のtileをcross axisに配置するためにStaggeredGridView.countがよく使われます。今回使用するのはこちらのコンストラクタです。また、 最大のcross axisへのextentを持つタイルで配置するためにStaggeredGridView.extentが使われます。metro grid styleな画面を実装するならこちらを使うのだと思います。
StaggeredTile
StaggeredGridViewのタイルの寸法を保持するクラスです。
StaggeredTileもユースケースに合わせたコンストラクタを主に3つ提供しています。StaggeredTile.countコンストラクタはcrossAxisCellCount
列、mainAxisCellCount
行のタイルを作成することを表します。静的にタイルのレイアウトを決めたい時はこちらを使うのだと思います。
2つ目のコンストラクタがStaggeredTile.extentコンストラクタです。こちらはドキュメントにある通りmain axisへの固定された範囲を保持したレイアウトを保持します。
3つ目のコンストラクタがStaggeredTIle.fitコンストラクタです。 crossAxisCellCount
を指定する必要があります。mainAxisへの範囲を内容にあわせて表示します。今回のPinterest風のレイアウトを実現するならこのコンストラクタを使うのが良さそうです。
SliverStaggeredGrid
StaggeredGridViewをSliver系のウィジェットと組み合わせて使えるようにするために提供されているクラスです。Sliver系ウィジェットは併用する時にCustomScrollView.sliversにListで渡すのでその時に使用できます。
StaggeredGridView.countの基本的な使い方
登場する基本的なクラスについて説明したので簡易的な実装をしてみます。
import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; void main() => runApp(MaterialApp( home: StaggeredExample(), theme: ThemeData( primaryColor: Colors.black, ), )); class StaggeredExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('StaggeredExample'), ), body: Padding( padding: EdgeInsets.only(top: 12.0), child: StaggeredGridView.count( crossAxisCount: 4, staggeredTiles: const <StaggeredTile>[ const StaggeredTile.count(3, 2), const StaggeredTile.count(1, 1), const StaggeredTile.count(1, 1), const StaggeredTile.count(2, 2), const StaggeredTile.count(2, 1), const StaggeredTile.count(1, 2), const StaggeredTile.count(1, 1), const StaggeredTile.count(2, 2), const StaggeredTile.count(1, 2), const StaggeredTile.count(1, 1), const StaggeredTile.count(3, 1), const StaggeredTile.count(1, 1), const StaggeredTile.count(4, 1), ], children: [ const _SampleTile(Colors.green), const _SampleTile(Colors.lightBlue), const _SampleTile(Colors.amber), const _SampleTile(Colors.brown), const _SampleTile(Colors.deepOrange), const _SampleTile(Colors.indigo), const _SampleTile(Colors.red), const _SampleTile(Colors.pink), const _SampleTile(Colors.purple), const _SampleTile(Colors.blue), const _SampleTile(Colors.black), const _SampleTile(Colors.red), const _SampleTile(Colors.brown), ], ), ), ); } } class _SampleTile extends StatelessWidget { const _SampleTile(this.backgroundColor); final Color backgroundColor; @override Widget build(BuildContext context) { return new Card( color: backgroundColor, child: new InkWell( onTap: () {}, child: new Center( child: new Padding( padding: const EdgeInsets.all(4.0), ), ), ), ); } }
静的に寸法を指定してContainerを使ってレイアウトを組みました。
crossAxisCountが4にしているので最大4のうちどれだけcross axisに対して専有するのか、main axisに対してどれだけ専有するのかをstaggeredTilesパラメータで指定しています。指定したList<StaggeredTile>
と同じ数のWidgetのListをchildrenパラメータに渡します。
これで静的にではありますがStaggeredGridViewの実装は済みました。このコードのstaggeredTileseパラメータの値やcrossAxisCountの値を変えることで自由にレイアウトを組み替えることができます。
Pinterest風のUIを作る
実現したいのはcross axisには決まった数(cross axisに対してちょうど半分)でmain axisにはコンテンツのサイズ分の範囲を持つTile上のレイアウトです。
そこで今回使うのは
- StaggeredGridView.countコンストラクタ
- StaggeredTile.fitコンストラクタ
の2つです。
画像について
UIのみの実装としたいので画像はassetとしてアプリケーションで保持します。Flutterで画像を追加する場合はpubspec.yamlで設定を行います。
今回はassetフォルダを画像を保持するフォルダとして設定したいのでassetsフォルダに画像を移動させた後、pubspec.yamlファイルに以下のように記述します。
# To add assets to your application, add an assets section, like this: assets: - assets/
記述後、flutter pub getコマンドを実行します。
※アプリ内で使用させていただいた画像それぞれへのリンク先です。
- May_hokkaidoによるPixabayからの画像
- Barney EloによるPixabayからの画像
- Phuc NguyenによるPixabayからの画像
- 토마스 정によるPixabayからの画像
- 서 은성によるPixabayからの画像
- Branimir LambašaによるPixabayからの画像
- wei zhuによるPixabayからの画像
- Ioannis IoannidisによるPixabayからの画像
- https://500px.com/ michael-gaidaによるPixabayからの画像
- Tim HillによるPixabayからの画像
- enriquelopezgarreによるPixabayからの画像
- HeidelbergerinによるPixabayからの画像
- analogicusによるPixabayからの画像
- S. Hermann & F. RichterによるPixabayからの画像
実装
class Home extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( centerTitle: true, iconTheme: IconThemeData( color: Colors.white, ), title: Text('Pinterst風UI Sample'), actions: <Widget>[ Container( margin: EdgeInsets.only(right: 10), child: Icon(Icons.message), ) ], ), body: StaggeredGridView.count( crossAxisCount: 4, children: List.generate(15, (index) { return _Tile(index); }), staggeredTiles: List.generate(15, (index) { return StaggeredTile.fit(2); }), ), ); } } class _Tile extends StatelessWidget { _Tile(this.index); final int index; @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(10), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(5)), child: Image.asset('assets/${1 + index}.jpg'), ), ); }
実行結果は以下になります。
StaggeredGridView.countコンストラクタのcrossAxisパラメータに4を渡してcross axis方向への全体的な寸法を指定しています。List.generateコンストラクタを使って_Tile
ウィジェットを指定の数もつListをchildrenパラメータに渡しています。_Tile
はパラメータにint型の値を受け取っていますがこれをString Interpolationで画像ファイルを指定するのに使っています。
staggeredTilesパラメータにもList.generateコンストラクタを使用しています。ここでは先述の通り要件にあわせてStaggeredTile.fitコンストラクタを、引数にはcross axisに指定した4に対してちょうど半分になるよう2を渡します。fitなのでmain axisへの範囲はコンテンツに依存します。実装の解説は以上になります。
その他クラスについて
今までに記事で扱っていないクラスがいくつか登場しているので簡単な解説とドキュメントへのリンクを併記します。Widgetの基本的な使い方を解説しているシリーズで扱った時にはリンクを書き換えたいと思っています。
ClipRRect
childに指定したウィジェットを矩形に切り取るWidgetです。borderRadiusプロパティで丸みを指定できます。ドキュメントはこちらです。
Padding
childに指定したWidgetにパディングをもたせることができるWidgetです。ドキュメントはこちらです。
InkWell
マテリアルデザインでアクション可能な部品をタップすると波紋のように広がるアニメーションがありますが、リップルエフェクトと呼びます。そのようなエフェクトを持つコントロールを実装する時に使用するWidgetです。ドキュメントはこちらです。
まとめ
初めて使用するクラスを使ってレイアウトを組む時には、試行錯誤を高速に繰り返せるHot Reloadが本当にありがたいです。Flutterは言語仕様がすごいシンプルで扱いやすいので日々の開発で得た経験を活かしてロジックを記述できていて不満を感じていません。一方思い通りのレイアウトを作るにはもう少しインプットが必要だなと感じているので良さそうなレイアウトをFlutterで作ってみることを繰り返しながら練習したいと考えています。